How to hot load jars for Spring Boot to implement dynamic plugins?
2021-10-19  skills  699 words  4 min read
Background
Dynamic plug-in programming is a cool thing to achieve decoupling of business functions for easy maintenance, in addition to enhance scalability can be extended at any time without stopping the server, but also has a very good openness in addition to their own R & D staff can develop features, but also to accept third-party developers in accordance with the specifications of the development of plug-ins.

The common implementation of dynamic plug-ins are SPI, OSGI and other solutions, which cannot be injected into the main program’s bean objects in the plug-in because they are not managed by Spring IOC.

This article introduces the idea of hot-loading jar packages in Spring Boot projects and registering them as bean objects to dynamically extend the functionality while supporting the injection of the main application’s beans into the plug-in to achieve a more powerful plug-in.

Hotloading jar packages
Dynamically loading a jar package through a specified link or path can be done using the addURL method of the URLClassLoader, with the following sample code.

ClassLoaderUtil

public class ClassLoaderUtil {
    public static ClassLoader getClassLoader(String url) {
        try {
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            if (!method.isAccessible()) {
                method.setAccessible(true);
            }
            URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());
            method.invoke(classLoader, new URL(url));
            return classLoader;
        } catch (Exception e) {
            log.error("getClassLoader-error", e);
            return null;
        }
    }
}
When creating the URLClassLoader, specify the current system ClassLoader as the parent class loader ClassLoader.getSystemClassLoader() This step is more critical to open the ClassLoader between the main program and the plug-in, and solve various problems when registering the plug-in into the IOC ClassNotFoundException problems when registering plugins into the IOC.

Dynamic Registration Bean
Register the implementation classes loaded in the plugin jar to Spring’s IOC, and also inject the beans already in the IOC into the plugin; two scenarios of implementation at program startup and runtime, respectively.

Bean registration at startup
Use the ImportBeanDefinitionRegistrar to dynamically register the plugin’s beans at Spring Boot startup, with the following sample code.

PluginImportBeanDefinitionRegistrar

public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    private final String targetUrl = "file:/D:/SpringBootPluginTest/plugins/plugin-impl-0.0.1-SNAPSHOT.jar";
    private final String pluginClass = "com.plugin.impl.PluginImpl";

    @SneakyThrows
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl);
        Class<?> clazz = classLoader.loadClass(pluginClass);
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        BeanDefinition beanDefinition = builder.getBeanDefinition();
        registry.registerBeanDefinition(clazz.getName(), beanDefinition);
    }
}
Runtime Registration Bean
The bean that dynamically registers the plugin at runtime is implemented by using the ApplicationContext object, with sample code as follows.

@GetMapping("/reload")
public Object reload() throws ClassNotFoundException {
		ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl);
		Class<?> clazz = classLoader.loadClass(pluginClass);
		springUtil.registerBean(clazz.getName(), clazz);
		PluginInterface plugin = (PluginInterface)springUtil.getBean(clazz.getName());
		return plugin.sayHello("test reload");
}
SpringUtil

@Component
public class SpringUtil implements ApplicationContextAware {
    private DefaultListableBeanFactory defaultListableBeanFactory;
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    }

    public void registerBean(String beanName, Class<?> clazz) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
    }

    public Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}
Summary
This article introduces the plug-in implementation idea by shared ClassLoader and dynamically registered bean way to open the class loader and Spring container between the plug-in and the main program, which makes it very convenient to achieve class interaction between the plug-in and the plug-in and between the plug-in and the main program, such as injecting the main program’s Redis and DataSource in the plug-in, calling the remote Dubbo interface and so on.

However, since there is no isolation of the ClassLoader between plug-ins, there may be problems such as class conflicts and version conflicts; and since the Class object in the ClassLoader cannot be destroyed, there is no way to dynamically modify the classes loaded into the ClassLoader unless the class name or class path is changed.


So this solution is more suitable for the plug-in data volume is not too much, with a better development specification, plug-in after testing before going online or release scenarios.